
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#ifndef WIN32
	#include <unistd.h>
#endif


/*
0000    5   STRING  "NESM",01Ah  ; denotes an NES sound format file
0005    1   BYTE    Version number (currently 01h)
0006    1   BYTE    Total songs   (1=1 song, 2=2 songs, etc)
0007    1   BYTE    Starting song (1= 1st song, 2=2nd song, etc)
0008    2   WORD    (lo/hi) load address of data (8000-FFFF)
000a    2   WORD    (lo/hi) init address of data (8000-FFFF)
000c    2   WORD    (lo/hi) play address of data (8000-FFFF)
000e    32  STRING  The name of the song, null terminated
002e    32  STRING  The artist, if known, null terminated
004e    32  STRING  The Copyright holder, null terminated
006e    2   WORD    (lo/hi) speed, in 1/1000000th sec ticks, NTSC (see text)
0070    8   BYTE    Bankswitch Init Values (see text, and FDS section)
0078    2   WORD    (lo/hi) speed, in 1/1000000th sec ticks, PAL (see text)
007a    1   BYTE    PAL/NTSC bits:
bit 0: if clear, this is an NTSC tune
bit 0: if set, this is a PAL tune
bit 1: if set, this is a dual PAL/NTSC tune
bits 2-7: not used. they *must* be 0
007b    1   BYTE    Extra Sound Chip Support
bit 0: if set, this song uses VRCVI
bit 1: if set, this song uses VRCVII
bit 2: if set, this song uses FDS Sound
bit 3: if set, this song uses MMC5 audio
bit 4: if set, this song uses Namco 106
bit 5: if set, this song uses Sunsoft FME-07
bits 6,7: future expansion: they *must* be 0
007c    4   ----    4 extra bytes for expansion (must be 00h)
0080    nnn ----    The music program/data follows
*/


#define BASIC_HEADER_SIZE 15
static const unsigned char sBasicHeader[BASIC_HEADER_SIZE] = { 0x01, 0x08, 0x0C, 0x08, 0xDA, 0x07, 0x9E, 0x32, 0x30, 0x34, 0x38, 0x30, 0x00, 0x00, 0x00 };

#define COMPRESSION_TYPE_COUNT 3
static const char* sCompressionTypes[COMPRESSION_TYPE_COUNT] = { "pucrunch", "exomizer", "none" };


void exitWithUsage()
{
	printf("\nUsage: nsf64 [-c compressiontype] [-ntsc] inputfile [outputfile]\n\nCompression types are:\n");
	for (int i = 0; i < COMPRESSION_TYPE_COUNT; i++)
		printf("  %s\n", sCompressionTypes[i]);
	printf("  (default: none)\n");
	
	exit(EXIT_FAILURE);
}


char ascii_to_screencode(char character)
{
	if (character >= 'A' && character <= 'Z')
		return character - 0x40;
	else
		return character;
}


char* sInputFilename = NULL;
char* sOutputFilename = NULL;
char* sCompressionType = NULL;
bool sUseNTSC = false;


void parseCommandLine(int argc, char** argv)
{
	for (int i = 1; i < argc; i++)
	{
		int next_index = i+1;
		if (strcmp(argv[i], "-c") == 0 && next_index<argc)
		{
			sCompressionType = argv[next_index];
			i = next_index;
			continue;
		}
		else if (strcmp(argv[i], "-ntsc") == 0)
		{
			sUseNTSC = true;
			continue;
		}
		else
		{
			sInputFilename = argv[i];
			if (next_index<argc)
				sOutputFilename = argv[next_index];
			
			break;
		}
	}
	
	if (sInputFilename == NULL)
	{
		fprintf(stderr, "No input file.\n");
		exitWithUsage();
	}
	
	if (sOutputFilename == NULL)
	{
		sOutputFilename = (char*) malloc(strlen(sInputFilename) + 5);
		sprintf(sOutputFilename, "%s.prg", sInputFilename);
	}
	
	if (sCompressionType != NULL)
	{
		bool valid_compression_type = false;
		for (int i = 0; i < COMPRESSION_TYPE_COUNT; i++)
		{
			if (strcmp(sCompressionType, sCompressionTypes[i]) == 0)
			{
				valid_compression_type = true;
				break;
			}
		}
		
		if (!valid_compression_type)
		{
			fprintf(stderr, "Invalid compression type.\n");
			exitWithUsage();
		}
	}
}



int main(int argc, char** argv)
{
	parseCommandLine(argc, argv);
	
	FILE* nsf_file = fopen(sInputFilename, "rb");
	if (nsf_file == NULL)
	{
		fprintf(stderr, "Input file can not be opened.\n");
		return -1;
	}

	fseek(nsf_file, 0L, SEEK_END);
	int nsf_size = ftell(nsf_file);
	fseek(nsf_file, 0L, SEEK_SET);
	
	unsigned char* nsf_buffer = (unsigned char*) malloc(nsf_size);
	
	fread(nsf_buffer, nsf_size, 1, nsf_file);
	fclose(nsf_file);

	if (nsf_buffer[0] != 'N' ||
		nsf_buffer[1] != 'E' ||
		nsf_buffer[2] != 'S' ||
		nsf_buffer[3] != 'M' ||
		nsf_buffer[4] != 0x1a)
	{
		fprintf(stderr, "Input file is not a valid .nsf file.\n");
		return -1;
	}
	
	if (nsf_buffer[5] != 0x01)
	{
		fprintf(stderr, "Input file is of an unsupported .nsf file version (%d)\n", nsf_buffer[5]);
		return -1;
	}

	if (nsf_buffer[0x7b] != 0x00)
	{
		fprintf(stderr, "Input file uses custom sound hardware which is not emulated\n");
		return -1;
	}
	
	int tune_size = nsf_size-0x80;

	unsigned char song_count = nsf_buffer[0x06];
	unsigned char default_song = nsf_buffer[0x07] - 1; 

	unsigned short load_addr = (nsf_buffer[0x09] << 8) | nsf_buffer[0x08];
	unsigned short init_addr = (nsf_buffer[0x0b] << 8) | nsf_buffer[0x0a]; 
	unsigned short play_addr = (nsf_buffer[0x0d] << 8) | nsf_buffer[0x0c]; 
	
	int end_addr = load_addr + tune_size;
	
	// load addr must be at least $8000
	if (load_addr < 0x8000 || end_addr >= 0xd000)
	{
		fprintf(stderr, "Input file uses unsupported memory range ($%04x - $%04x)\n", load_addr, end_addr);
		return -1;
	}
	
	printf("Valid tune, load=$%04x, init=$%04x, play=$%04x\n", load_addr, init_addr, play_addr);

	bool is_banked = false;
	for (int i = 0; i < 8; i++)
	{
		if (nsf_buffer[0x70+i] != 0)
		{
			is_banked = true;
			break;
		}
	}
	
	unsigned char* c64_memory = (unsigned char*) malloc(64*1024);
	memset(c64_memory, 0, 64*1024);
	
	// Load tune data into c64 memory
	if (is_banked)
	{
		printf("Tune is banked: %02x %02x %02x %02x %02x %02x %02x %02x\n", nsf_buffer[0x70], nsf_buffer[0x71], nsf_buffer[0x72], nsf_buffer[0x73],
																		    nsf_buffer[0x74], nsf_buffer[0x75], nsf_buffer[0x76], nsf_buffer[0x77]);
		
		// load into banked memory
		unsigned char* banked_memory = (unsigned char*) malloc(32*1024);

		memcpy(&banked_memory[load_addr & 0x0fff], &nsf_buffer[0x80], tune_size);

		// reorder banks
		for (int i = 0; i < 8; i++)
		{
			int bank_id = nsf_buffer[0x70 + i];
			memcpy(&c64_memory[0x8000 + i*0x1000], &banked_memory[bank_id*0x1000], 0x1000);
		}

		tune_size = 0x8000;
	}
	else
		memcpy(&c64_memory[load_addr], &nsf_buffer[0x80], tune_size);

	// Load player shell into c64 memory
	FILE* player_file = fopen(sUseNTSC ? "player_shell_ntsc.bin" : "player_shell.bin", "rb");
	if (player_file == NULL)
	{
		fprintf(stderr, "Can not open player shell file.\n");
		return -1;
	}
	
	fseek(player_file, 0L, SEEK_END);
	int player_size = ftell(player_file);
	fseek(player_file, 0L, SEEK_SET);
	
	unsigned char player_lo_addr = 0;
	unsigned char player_hi_addr = 0;
	
	fread(&player_lo_addr, 1, 1, player_file);
	fread(&player_hi_addr, 1, 1, player_file);

	unsigned short player_addr = (player_hi_addr << 8) | player_lo_addr;

	printf("Player starts at $%04x\n", player_addr);

	fread(&c64_memory[player_addr], player_size, 1, player_file);
	fclose(player_file);

	for (int i = 0; i < 32; i++)
	{
		char title_character = nsf_buffer[0x0e + i];
		if (title_character == 0)
			break;
		
		c64_memory[0x5f00+i] = ascii_to_screencode(toupper(title_character));
	}

	for (int i = 0; i < 32; i++)
	{
		char author_character = nsf_buffer[0x2e + i];
		if (author_character == 0)
			break;
		
		c64_memory[0x5f28+i] = ascii_to_screencode(toupper(author_character));
	}

	for (int i = 0; i < 32; i++)
	{
		char release_character = nsf_buffer[0x4e + i];
		if (release_character == 0)
			break;
		
		c64_memory[0x5f50+i] = ascii_to_screencode(toupper(release_character));
	}
	
	c64_memory[0x5ffa] = song_count;
	c64_memory[0x5ffb] = default_song;
	c64_memory[0x5ffc] = init_addr & 0x00ff;
	c64_memory[0x5ffd] = init_addr >> 8;
	c64_memory[0x5ffe] = play_addr & 0x00ff;
	c64_memory[0x5fff] = play_addr >> 8;
	
	int output_size = end_addr - player_addr;
	int rc = 0;
	
	char crunch_cmdline[1024];
	
	if (sCompressionType != NULL && strcmp(sCompressionType, "none") != 0)
	{
		printf("--------------------------------------------------------------\n");
	
		// Write temporary output file of uncompressed data
		FILE* output_file = fopen("output.bin", "wb");
		fwrite(&player_lo_addr, 1, 1, output_file);
		fwrite(&player_hi_addr, 1, 1, output_file);
		fwrite(&c64_memory[player_addr], output_size, 1, output_file);
		fclose(output_file);

		if (strcmp(sCompressionType, "exomizer") == 0)
		{
			sprintf(crunch_cmdline, "exomizer sfx 0x%04x -t 64 -n -o \"%s\" output.bin", player_addr, sOutputFilename);
			rc = system(crunch_cmdline);
		}
		else if (strcmp(sCompressionType, "pucrunch") == 0)
		{
			sprintf(crunch_cmdline, "pucrunch output.bin \"%s\" -c64 -m5 -fs -p0 -x0x%04x -g$37", sOutputFilename, player_addr);
			rc = system(crunch_cmdline);
		}
		
		unlink("output.bin");
	
		printf("--------------------------------------------------------------\n");
	}
	else 
	{
		// just add basic line
		memcpy(&c64_memory[0x0801-2], sBasicHeader, BASIC_HEADER_SIZE);

		FILE* output_file = fopen(sOutputFilename, "wb");
		fwrite(&c64_memory[0x0801-2], output_size + (player_addr - 0x0801), 1, output_file);
		fclose(output_file);
	}

	if (rc == 0)
	{
		printf("Success!\n");
		printf("Output written to %s\n", sOutputFilename);
		return 0;
	}
	else
	{
		fprintf(stderr, "Could not compress output file.\n");
		return -1;
	}
	
	return 0;
}